
#include "GLDebugger.hpp"
#include "MatrixSystem.hpp"

#include <GLFW/glfw3.h>
#include <glad/glad.h>

#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"

#include "AttriBinding.hpp"
#include "Model.hpp"

#include <iostream>
#include <string>

void compileShader(GLuint shaderId, const char *src) {
  glShaderSource(shaderId, 1, &src, nullptr);
  glCompileShader(shaderId);
  GLint params;
  glGetShaderiv(shaderId, GL_COMPILE_STATUS, &params);
  if (params == 0) {
    GLint typeId;
    glGetShaderiv(shaderId, GL_SHADER_TYPE, &typeId);
    GLchar info[255];
    GLsizei length;
    if (typeId == GL_VERTEX_SHADER) {
      std::cout << "Vertex Shader";
    } else if (typeId == GL_FRAGMENT_SHADER) {
      std::cout << "Fragment Shader";
    }
    std::cout << " Compile Failed!" << std::endl;
    glGetShaderInfoLog(shaderId, 255, &length, info);
    std::cout << info << std::endl;
  }
}

void QueryProgram(GLuint program) {
  if (0 == glIsProgram(program)) {
    std::cout << "The program is invalid!" << std::endl;
  }
  static constexpr GLenum interfaces[] = {GL_BUFFER_VARIABLE, GL_UNIFORM};
  const std::string interface_name[] = {"Buffers", "Uniforms"};
  const std::string resource_name[] = {"vertexPos", "mvp"};
  static GLchar names[255] = {0};
  for (int i = 0; i < 2; i++) {
    GLint resources;
    glGetProgramInterfaceiv(program, interfaces[i], GL_ACTIVE_RESOURCES, &resources);
    std::cout << "Active " << interface_name[i] << '\n'
              << "- Num : \t" << resources << std::endl;
    GLuint index =
        glGetProgramResourceIndex(program, interfaces[i], resource_name[i].c_str());
    if (index != GL_INVALID_INDEX) {
      glGetProgramResourceName(program, interfaces[i], index, 255, nullptr, names);
    }
  }
}

void WindowResizeCB(GLFWwindow *window, int w, int h) {
  glViewport(0, 0, w, h);
  glDepthRange(0.1f, 10.f);
}

float scrollY = 1.0f;
float sensity = 5.5f;

double deltaTime = 0;
double lastTime = 0;

void WindowScrollCB(GLFWwindow *window, double x, double y) {
  scrollY -= float(y * sensity);
}

void WindowMouseButtonCB(GLFWwindow *window, int button, int action, int mods) {
  if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS) {
  }
}

void WindowKeyBoardCB(GLFWwindow *window, int key, int scancode, int action, int mods) {}

constexpr GLint screen_width = 960;
constexpr GLint screen_height = 720;

int main() {

  // 初始化GLFW
  glfwInit();

  glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);

  GLFWwindow *window =
      glfwCreateWindow(screen_width, screen_height, "App", nullptr, nullptr);
  if (window == nullptr) {
    glfwTerminate();
    return -1;
  }
  glfwSetWindowSizeCallback(window, WindowResizeCB);
  glfwSetKeyCallback(window, WindowKeyBoardCB);
  glfwSetScrollCallback(window, WindowScrollCB);
  glfwSetMouseButtonCallback(window, WindowMouseButtonCB);
  glfwMakeContextCurrent(window);
  gladLoadGL();
  glEnable(GL_DEBUG_OUTPUT);
  glDebugMessageCallback(Debug::MessageCallback, nullptr);

  Data::Model cube("Cube");
  Data::MeshLoader loader(&cube);
  loader.LoadMeshFromFile("./data/cube.obj");
  auto cubeMesh = cube.getMesh();
  GLuint buffers[3] = {0};
  glCreateBuffers(3, buffers);
  glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
  glNamedBufferData(buffers[0], Data::VERTEX_STRIDE * cubeMesh->getVertexCount(),
                    cubeMesh->getVertexRawData(), GL_STREAM_DRAW);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[1]);
  glNamedBufferData(buffers[1], sizeof(GLuint) * cubeMesh->getIndicesCount(),
                    cubeMesh->getIndicesRawData(), GL_STATIC_DRAW);

  glm::vec3 translations[] = {{1.0f, 0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}};
  glBindBuffer(GL_ARRAY_BUFFER, buffers[2]);
  glNamedBufferData(buffers[2], sizeof(glm::vec3) * 2, glm::value_ptr(translations[0]),
                    GL_STATIC_DRAW);

  GLuint vao;
  enum AttributeIndex { Position, Normal, Texcoordinate, instanceOffset };
  glCreateVertexArrays(1, &vao);
  glBindVertexArray(vao);
  glEnableVertexArrayAttrib(vao, AttributeIndex::Position);
  glEnableVertexArrayAttrib(vao, AttributeIndex::Normal);
  glEnableVertexArrayAttrib(vao, AttributeIndex::Texcoordinate);
  glEnableVertexArrayAttrib(vao, AttributeIndex::instanceOffset);

  glVertexArrayAttribFormat(vao, AttributeIndex::Position, 3, GL_FLOAT, GL_FALSE,
                            Data::POSITION_OFFSET);
  glVertexArrayAttribFormat(vao, AttributeIndex::Normal, 3, GL_FLOAT, GL_FALSE,
                            Data::NORMAL_OFFSET);
  glVertexArrayAttribFormat(vao, AttributeIndex::Texcoordinate, 2, GL_FLOAT, GL_FALSE,
                            Data::TEXCOORS_OFFSET);
  glVertexArrayAttribFormat(vao, AttributeIndex::instanceOffset, 3, GL_FLOAT, GL_FALSE,
                            0);

  GLuint bindingIndex[] = {0, 1};
  glVertexArrayAttribBinding(vao, AttributeIndex::Position, bindingIndex[0]);
  glVertexArrayAttribBinding(vao, AttributeIndex::Normal, bindingIndex[0]);
  glVertexArrayAttribBinding(vao, AttributeIndex::Texcoordinate, bindingIndex[0]);
  glVertexArrayAttribBinding(vao, AttributeIndex::instanceOffset, bindingIndex[1]);

  {
    glVertexArrayBindingDivisor(vao, bindingIndex[1], 1);
    // Get Divisor Count
    GLint max_divisors;
    glGetIntegeri_v(GL_VERTEX_BINDING_DIVISOR, bindingIndex[1], &max_divisors);
    std::cout << "Current Vertex Binding Divisor: " << max_divisors << std::endl;
    // Binding Divisor Add one attribute advance instance
  }

  glVertexArrayVertexBuffer(vao, bindingIndex[0], buffers[0], 0, Data::VERTEX_STRIDE);
  glVertexArrayVertexBuffer(vao, bindingIndex[1], buffers[2], 0, sizeof(glm::vec3));
  glVertexArrayElementBuffer(vao, buffers[1]);

  GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
  constexpr char *vertexSrc =
      "#version 450\n"
      "in vec3 vertexPos;\n"
      "in vec3 vertexNormal;\n"
      "in vec2 texCoords;\n"
      "in vec3 instancedOffset;\n"
      "out vec3 color;\n"
      "uniform mat4 mvp;\n"
      "void main()\n"
      "{\n"
      " color = vertexNormal;\n"
      " //if(gl_InstanceID == 1) {\n"
      " // gl_Position = mvp * vec4(vertexPos + vec3(0.5, 0.0, 0.0), 1.0);\n"
      "// }\n"
      " gl_Position = mvp * vec4(vertexPos + instancedOffset, 1.0);\n"
      "}\n";
  compileShader(vertexShader, vertexSrc);

  GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
  constexpr char *fragSrc =
      "#version 450\n "
      "out vec4 FragColor;\n"
      "in vec3 color;"
      "void main()\n"
      "{\n"
      " if (gl_FrontFacing) FragColor = vec4(vec3(0.0, 0.0, 1.0), 1.0);\n"
      " else FragColor = vec4(vec3(1.0, 0.0, 0.0), 1.0);\n"
      "}\n";
  compileShader(fragShader, fragSrc);

  GLuint program = glCreateProgram();
  glAttachShader(program, vertexShader);
  glAttachShader(program, fragShader);
  glValidateProgram(program);
  glBindAttribLocation(program, AttributeIndex::Position, "vertexPos");
  glBindAttribLocation(program, AttributeIndex::Normal, "vertexNormal");
  glBindAttribLocation(program, AttributeIndex::Texcoordinate, "texCoords");
  glBindAttribLocation(program, AttributeIndex::instanceOffset, "instancedOffset");
  glLinkProgram(program);

  glDetachShader(program, vertexShader);
  glDetachShader(program, fragShader);
  glDeleteShader(vertexShader);
  glDeleteShader(fragShader);

  // QueryProgram(program);
  {
    GLchar names[50] = {0};
    GLint bufsize;
    GLenum bufType;

    glGetActiveAttrib(program, AttributeIndex::Position, 50, nullptr, &bufsize, &bufType,
                      names);
    std::cout << "Active Attributes: \n"
              << "- Name: " << names << '\n';
    std::cout << "- Size: " << bufsize << '\n';
    std::cout << "- Type: " << (bufType == GL_FLOAT_VEC3 ? "FLOAT_VEC3" : "Unknown")
              << std::endl;
    glGetActiveUniform(program, glGetUniformLocation(program, "mvp"), 50, nullptr,
                       &bufsize, &bufType, names);
    std::cout << "Active Uniforms: \n"
              << "- Name: " << names << '\n';
    std::cout << "- Size: " << bufsize << '\n';
    std::cout << "- Type: " << (bufType == GL_FLOAT_MAT4 ? "MAT4" : "Unknown")
              << std::endl;
  }

  IMGUI_CHECKVERSION();
  ImGui::SetCurrentContext(ImGui::CreateContext());
  ImGui_ImplOpenGL3_Init("#version 450");
  ImGui_ImplGlfw_InitForOpenGL(window, false);

  ImGui::StyleColorsDark();

  constexpr float clear_color[4] = {0.f, 0.f, 0.f, 1.f};

  glClearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
  constexpr float origin_aspect = float(screen_width) / screen_height;
  double baseFrequency = 1.0 / 60;
  double current_time = baseFrequency;
  deltaTime = baseFrequency;
  while (!glfwWindowShouldClose(window)) {
    lastTime = glfwGetTime();

    glfwPollEvents();
    glClear(GL_COLOR_BUFFER_BIT);

    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    static int drawVerts = int(cube.getMesh()->getIndicesCount());
    static float rotate = 0.f;
    static bool IsLineMode = false;
    static glm::vec3 offset[2];
    if (ImGui::Begin("Hello Imgui")) {
      ImGui::Text("ImGui Average Frame rate: %.2f", 1 / ImGui::GetIO().DeltaTime);
      ImGui::Text("My Average Frame rate: %.5f", 1 / deltaTime);
      ImGui::SliderInt("Draw Count", &drawVerts, 1,
                       int(cube.getMesh()->getIndicesCount()));
      ImGui::SliderFloat("Rotate along y axis", &rotate, -60, 180);
      ImGui::SliderFloat3("Instance 1", glm::value_ptr(offset[0]), -2.f, 2.f);
      ImGui::SliderFloat3("Instance 2", glm::value_ptr(offset[1]), -2.f, 2.f);
      ImGui::Checkbox("LineMode", &IsLineMode);
    }
    ImGui::End();
    ImGui::Render();

    glm::vec3 *translations =
        reinterpret_cast<glm::vec3 *>(glMapNamedBuffer(buffers[2], GL_WRITE_ONLY));
    translations[0] = offset[0];
    translations[1] = offset[1];
    glUnmapNamedBuffer(buffers[2]);

    GLint w, h;
    glfwGetWindowSize(window, &w, &h);
    float aspect = (w * h == 0 ? origin_aspect : float(w) / h);
    float val = (scrollY * float(baseFrequency));

    GetMatrixA.setMatrixMode(Data::MatrixMode::MODELVIEW_MATRIX);
    GetMatrixA.setCurrentIdentity();
    GetMatrixA.rotate(glm::vec3(0.f, 1.f, 0.f), glm::radians(rotate));
    GetMatrixA.multiMatrix(glm::lookAt(val * glm::vec3(1.0, 1.0, 1.0), glm::vec3(0),
                                       glm::vec3(0.0, 1.0, 0.0)));

    GetMatrixA.setMatrixMode(Data::MatrixMode::PERSPECTIVE_MATRIX);
    GetMatrixA.setCurrentMatrix(glm::perspective(45.f, aspect, 0.01f, 10.f));

    glUseProgram(program);
    glBindVertexArray(vao);
    GLuint loc = glGetUniformLocation(program, "mvp");

    glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(GetMatrixA.getMVP()));
    glPolygonMode(GL_FRONT_AND_BACK, IsLineMode ? GL_LINE : GL_FILL);
    // glDrawElements(GL_TRIANGLES, drawVerts, GL_UNSIGNED_INT, nullptr);
    glDrawElementsInstanced(GL_TRIANGLES, drawVerts, GL_UNSIGNED_INT, nullptr, 2);
    glBindVertexArray(0);
    glUseProgram(0);
    GetMatrixA.setMatrixMode(Data::MatrixMode::MODELVIEW_MATRIX);

    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

    glfwSwapBuffers(window);
    current_time = glfwGetTime() - lastTime;
    deltaTime =
        (glm::min(baseFrequency, (current_time)) + deltaTime + baseFrequency) / 3.0;
  }

  ImGui_ImplOpenGL3_Shutdown();
  ImGui_ImplGlfw_Shutdown();
  glDeleteBuffers(2, buffers);
  glDeleteProgram(program);
  glDeleteVertexArrays(1, &vao);
  glfwDestroyWindow(window);
  glfwTerminate();
}